import { invertTransform, multiplyTransformMatrices, qrDecompose} from '../util/misc/matrix';
import { removeTransformMatrixForSvgParsing } from '../util/transform_matrix_removal';
import { CENTER } from '../constants';
import { getGradientDefs } from './getGradientDefs';
import { getCSSRules } from './getCSSRules';
import { Gradient, Group, classRegistry, Point } from 'fabric';
import { Image } from '../object/Image';
import { IText } from '../object/IText';
import { nanoid } from 'nanoid';

const findTag = (el) => {
  const tag = el.tagName.toLowerCase().replace('svg:', '')
  if (tag === 'image') return Image
  if (tag === 'text') return IText
  return classRegistry.getSVGClass(el.tagName.toLowerCase().replace('svg:', ''));
}

export class ElementsParser {
  constructor(elements, options, reviver, doc, clipPaths, masks) {
    this.elements = elements;
    this.options = options;
    this.reviver = reviver;
    this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g;
    this.doc = doc;
    this.clipPaths = clipPaths;
    this.masks = masks;
    this.gradientDefs = getGradientDefs(doc);
    this.cssRules = getCSSRules(doc);
  }

  parse() {
    return Promise.all(
      this.elements.map((element) => this.createObject(element))
    );
  }

  async createObject(el) {
    const klass = findTag(el);
    if (klass) {
      const obj = await klass.fromElement(
        el,
        this.options,
        this.cssRules
      );
      obj.set({id: nanoid(10)})
      this.resolveGradient(obj, el, 'fill');
      this.resolveGradient(obj, el, 'stroke');
      if (obj instanceof Image && obj._originalElement) {
        removeTransformMatrixForSvgParsing(
          obj,
          obj.parsePreserveAspectRatioAttribute()
        );
      } else {
        removeTransformMatrixForSvgParsing(obj);
      }
      await this.resolveClipPath(obj, el);
      await this.resolveMask(obj, el);
      this.reviver && this.reviver(el, obj);
      return obj;
    }
    return null;
  }

  extractPropertyDefinition( obj, property, storage ) {
    const value = obj[property],
      regex = this.regexUrl;
    if (!regex.test(value)) {
      return undefined;
    }
    // verify: can we remove the 'g' flag? and remove lastIndex changes?
    regex.lastIndex = 0;
    // we passed the regex test, so we know is not null;
    const id = regex.exec(value)[1];
    regex.lastIndex = 0;
    // @todo fix this
    return storage[id];
  }

  resolveGradient( obj, el, property) {
    const gradientDef = this.extractPropertyDefinition(
      obj,
      property,
      this.gradientDefs
    );
    if (gradientDef) {
      const opacityAttr = el.getAttribute(property + '-opacity');
      const gradient = Gradient.fromElement(gradientDef, obj, {
        ...this.options,
        opacity: opacityAttr,
      });
      obj.set(property, gradient);
    }
  }

  async resolveClipPath(obj, usingElement) {
    const clipPathElements = this.extractPropertyDefinition(
      obj,
      'clipPath',
      this.clipPaths
    );
    if (clipPathElements) {
      const objTransformInv = invertTransform(obj.calcTransformMatrix());
      // move the clipPath tag as sibling to the real element that is using it
      const clipPathTag = clipPathElements[0].parentElement;
      let clipPathOwner = usingElement;
      while (
        clipPathOwner.parentElement &&
        clipPathOwner.getAttribute('clip-path') !== obj.clipPath
      ) {
        clipPathOwner = clipPathOwner.parentElement;
      }
      clipPathOwner.parentElement.appendChild(clipPathTag);
      const container = await Promise.all(
        clipPathElements.map((clipPathElement) => {
          return findTag(clipPathElement)
            .fromElement(clipPathElement, this.options, this.cssRules)
            .then((enlivedClippath) => {
              removeTransformMatrixForSvgParsing(enlivedClippath);
              enlivedClippath.fillRule = enlivedClippath.clipRule;
              delete enlivedClippath.clipRule;
              return enlivedClippath;
            });
        })
      );
      const clipPath = container.length === 1 ? container[0] : new Group(container);
      const gTransform = multiplyTransformMatrices(
        objTransformInv,
        clipPath.calcTransformMatrix()
      );
      if (clipPath.clipPath) {
        await this.resolveClipPath(clipPath, clipPathOwner);
      }
      const { scaleX, scaleY, angle, skewX, translateX, translateY } = qrDecompose(gTransform);
      clipPath.set({
        flipX: false,
        flipY: false,
      });
      clipPath.set({
        scaleX,
        scaleY,
        angle,
        skewX,
        skewY: 0,
      });
      clipPath.setPositionByOrigin(
        new Point(translateX, translateY),
        CENTER,
        CENTER
      );
      obj.clipPath = clipPath;
    } else {
      // if clip-path does not resolve to any element, delete the property.
      delete obj.clipPath;
      return;
    }
  }

  async resolveMask(obj, usingElement) {
    const maskElements = this.extractPropertyDefinition(
      obj,
      'mask',
      this.masks
    );
    if (maskElements) {
      const maskElement = maskElements[0]
      const maskImage = await Image.fromElement(maskElement)
      obj.set({mask: {
        src: maskImage?.getSrc(),
        left: obj.left,
        top: obj.top,
        width: obj.width,
        height: obj.height
      }})
      // if (obj instanceof Image && obj._originalElement) {
      //   console.log('maskImage:', maskImage, 'obj:', obj.id)
      //   const [ pixi ] = usePixi()
      //   pixi.postMessage({
      //     id: obj.id,
      //     type: "mask", 
      //     src: obj.getSrc(),
      //     mask: JSON.stringify({
      //       src: maskImage?.getSrc()
      //     }), 
      //     width: obj.width, 
      //     height: obj.height
      //   });
      // }
    }
  }
}
